from django.shortcuts import render, get_object_or_404
from .models import Post, Comment
from django.http import Http404
from django.core.paginator import (Paginator, #встроенный класс постраничной разбивки (пагинации)
                                EmptyPage, # При извлечении страницы 3 объект Paginator выдает исключение EmptyPage,поскольку она находится вне диапазона.
                                PageNotAnInteger, # Данное представление также должно обрабатывать случай, когда в параметре page передается нечто отличное от целого числа.
                                )
from django.views.generic import ListView # Использование представления на основе класса для отображения списка постов
from django.contrib.postgres.search import SearchVector # Полнотекстовый поиск
from .forms import EmailPostForm, CommentForm,  SearchForm # импорт форм в представления
from django.core.mail import send_mail 
# Функция send_mail() принимает тему, сообщение, отправителя и список
# получателей в качестве требуемых аргументов.
from django.views.decorators.http import require_POST
from taggit.models import Tag # импортируйте модель Tag из приложения django-taggit и измените представление post_list
from django.db.models import Count # Извлечение постов по сходству
# Выделение основ слов и ранжирование результатов
from django.contrib.postgres.search import SearchVector, \
SearchQuery, SearchRank 
# Поиск по триграммному сходству
from django.contrib.postgres.search import TrigramSimilarity

# Create your views here.
# Представление списка постов
# Это самое первое представление Django. Представление post_list
# принимает объект request в качестве единственного параметра. Указанный
# параметр необходим для всех функций-представлений.
# В данном представлении извлекаются все посты со статусом PUBLISHED, ис-
# пользуя менеджер published, который мы создали ранее
# def post_list(request):
#     posts = Post.published.all()
#     return render(request, 'blog/post/list.html', {'posts': posts})
    # render(), чтобы прорисовать посты posts заданным шаблоном list.html

    # Представление одиночного поста
    # Это представление детальной информации о посте. Указанное представ-
    # ление принимает аргумент id поста. Здесь мы пытаемся извлечь объект Post
    # с заданным id, вызвав метод get() стандартного менеджера objects. Мы
    # создаем исключение Http404, чтобы вернуть ошибку HTTP с кодом состоя-
    # ния, равным 404, если возникает исключение DoesNotExist, то есть модель не
    # существует, поскольку результат не найден.

# Меняем для пагинации
# Меняем для тегирования, чтобы пользователи
# имели возможность отображать список всех постов, помеченных конкретным тегом.
def post_list(request, tag_slug=None):
    post_list = Post.published.all()
    tag = None
    # Тегирование. Представление принимает опциональный параметр tag_slug, значение
    # которого по умолчанию равно None. Этот параметр будет передан в URL-
    # адресе. 
    if tag_slug:
        # Тегирование. Внутри указанного представления формируется изначальный набор
        # запросов, извлекающий все опубликованные посты, и если имеется
        # слаг данного тега, то берется объект Tag с данным слагом, используя
        # функцию сокращенного доступа get_object_or_404().
        tag = get_object_or_404(Tag, slug=tag_slug)
        # Тегирование. Затем список постов фильтруется по постам, которые содержат дан-
        # ный тег. Поскольку здесь используется взаимосвязь многие-ко-многим,
        # необходимо фильтровать записи по тегам, содержащимся в заданном
        # списке, который в данном случае содержит только один элемент. Здесь
        # используется операция __in поиска по полю. Взаимосвязи многие-ко-
        # многим возникают, когда несколько объектов модели ассоциированы
        # с несколькими объектами другой модели. В нашем приложении пост
        # может иметь несколько тегов, и тег может быть связан с несколькими
        # постами.
        post_list = post_list.filter(tags__in=[tag])
    # Постраничная разбивка с 3 постами на страницу
    paginator = Paginator(post_list, 3)
    page_number = request.GET.get('page', 1)
    # Обработка ошибок постраничной разбивки
    try:
        posts = paginator.page(page_number)
    except PageNotAnInteger:
        # Если page_number не целое число, то
        # выдать первую страницу
        posts = paginator.page(1)
    except EmptyPage:
        # Если page_number находится вне диапазона, то
        # выдать последнюю страницу
        posts = paginator.page(paginator.num_pages)
        # получаем общее число страниц посредством paginator.num_pages. 
        # Общее число страниц совпадает с номером последней страницы.

    return render(request,
                'blog/post/list.html',
                {'posts': posts,
                'tag': tag}) # Наконец, теперь функция render() передает новую переменную tag в шаблон
    # Откройте файл urls.py приложения blog, закомментируйте основанный на
    # классе шаблон URL-адреса PostListView и раскомментируйте представление post_list
# Давайте рассмотрим новый исходный код, который был добавлен в пред-
# ставление.
# 1. Мы создаем экземпляр класса Paginator с числом объектов, возвраща-
# емых в расчете на страницу. Мы будем отображать по три поста на
# страницу.
# 2. Мы извлекаем HTTP GET-параметр page и сохраняем его в переменной
# page_number. Этот параметр содержит запрошенный номер страницы.
# Если параметра page нет в GET-параметрах запроса, то мы используем
# стандартное значение 1, чтобы загрузить первую страницу результатов.
# 3. Мы получаем объекты для желаемой страницы, вызывая метод page()
# класса Paginator. Этот метод возвращает объект Page, который хранится
# в переменной posts.
# 4. Мы передаем номер страницы и объект posts в шаблон.

# Далее нужно выполнить 
# Создание шаблона постраничной разбивки pagination.html

# def post_detail(request, id):
#     try:
#         post = Post.published.get(id=id)
#     except Post.DoesNotExist:
#         raise Http404("No post found.")

#     return render(request,
#                 'blog/post/detail.html',
#                 {'post': post})
# Меняем для
# Создание дружественных для поисковой оптимизации
# URL-адресов постов см. пояснения в models.py
def post_detail(request, year, month, day, post):
    post = get_object_or_404(Post,
                            status=Post.Status.PUBLISHED,
                            slug=post,
                            publish__year=year,
                            publish__month=month,
                            publish__day=day)
    # Список активных комментариев к этому посту
    comments = post.comments.filter(active=True) # мы добавили набор запросов QuerySet, чтобы извлекать все активные комментарии к посту,
    #     •• этот набор запросов сформирован с использованием объекта post. Вместо
    # того чтобы формировать набор запросов для комментарной модели
    # напрямую, мы используем объект post, чтобы извлекать связанные объ-
    # екты Comment. Мы применяем менеджер comments для ранее определен-
    # ных в модели Comment связанных с Comment объектов, используя атрибут
    # related_name поля ForeignKey в модели Post;
    # •• мы также создали экземпляр формы для комментария посредством
    # инструкции form = CommentForm().     # Далее необходимо отредактировать шаблон blog/post/detail.html,
    # Форма для комментирования пользователями
    form = CommentForm()
    # - - - - - - - - - - - - - 
    # Извлечение постов по сходству
    post_tags_ids = post.tags.values_list('id', flat=True)
    similar_posts = Post.published.filter(tags__in=post_tags_ids)\
                                            .exclude(id=post.id)
    similar_posts = similar_posts.annotate(same_tags=Count('tags'))\
                                            .order_by('-same_tags','-publish')[:4]
    # Приведенный выше исходный код делает следующее:
    # 1) извлекается Python’овский список идентификаторов тегов текущего
    # поста. Набор запросов QuerySet values_list() возвращает кортежи со
    # значениями заданных полей. Ему передается параметр flat=True, чтобы
    # получить одиночные значения, такие как [1, 2, 3, ...], а не одноэле-
    # ментые кортежи, такие как [(1,), (2,), (3,) ...];
    # 2) берутся все посты, содержащие любой из этих тегов, за исключением
    # текущего поста;
    # 3) применяется функция агрегирования Count. Ее работа – генерировать
    # вычисляемое поле – same_tags, – которое содержит число тегов, общих
    # со всеми запрошенными тегами;
    # 4) результат упорядочивается по числу общих тегов (в убывающем по-
    # рядке) и по publish, чтобы сначала отображать последние посты для
    # постов с одинаковым числом общих тегов. Результат нарезается, чтобы
    # получить только первые четыре поста;
    # 5) объект similar_posts передается в контекстный словарь для функции
    # render().
    # Теперь отредактируйте шаблон blog/post/detail.html,
    # - - - - - - - - - - - - - 

    return render(request,
                'blog/post/detail.html',
                {'post': post,
                'comments': comments,
                'form': form,
                'similar_posts': similar_posts})

def post_detail_404(request, id):
    post = get_object_or_404(Post,
                            id=id,
                            status=Post.Status.PUBLISHED)
    return render(request,
                'blog/post/detail.html',
                {'post': post})

# ПАГИНАЦИЯ
# Добавление постраничной разбивки
# в представление списка постов

# - - - - - - - - - - - - - - - - -
# Обработка ошибок постраничной разбивки
# Для этого добавляем конструкции try-exept в функцию .views.post_list(request) см. выше  

# - - - - - - - - - - - - - - - - -
# Использование представления на основе класса
# для отображения списка постов

# В целях понимания того, как писать представления на основе классов, мы
# создадим новое представление на основе класса, эквивалентное представ-
# лению post_list. Мы создадим класс, который будет наследовать от предла-
# гаемого веб-фреймворком Django типового представления ListView. Пред-
# ставление ListView позволяет перечислять объекты любого типа.
# Отредактируйте файл views.py приложения blog, добавив следующий ниже
# исходный код:

# class PostListView(ListView):
#     """
#     Альтернативное представление списка постов
#     """
#     queryset = Post.published.all()
#     #     атрибут queryset используется для того, чтобы иметь конкретно-при-
#     # кладной набор запросов QuerySet, не извлекая все объекты. Вместо
#     # определения атрибута queryset мы могли бы указать model=Post, и Django
#     # сформировал бы для нас типовой набор запросов Post.objects.all();
#     context_object_name = 'posts'
#     #     контекстная переменная posts используется для результатов запроса.
#     # Если не указано имя контекстного объекта context_object_name, то по
#     # умолчанию используется переменная object_list;
#     paginate_by = 3
#     #     в атрибуте paginate_by задается постраничная разбивка результатов
#     # с возвратом трех объектов на страницу;
#     template_name = 'blog/post/list.html'
#     #     конкретно-прикладной шаблон используется для прорисовки страницы
#     # шаблоном template_name. Если шаблон не задан, то по умолчанию List-
#     # View будет использовать blog/post_list.html.

#     # Теперь отредактируйте файл urls.py приложения blog

# - - - - - - - - - - - - - - - - -
# Рекомендация постов по электронной почте
# Django поставляется с двумя базовыми классами для разработки форм:
# •• Form: позволяет компоновать стандартные формы путем определения
# полей и валидаций;
# •• ModelForm: позволяет компоновать формы, привязанные к экземплярам
# модели. Он предоставляет все функциональности базового класса Form,
# но поля формы можно объявлять явным образом или автоматически
# генерировать из полей модели. Форму можно использовать для созда-
# ния либо редактирования экземпляров модели.

# Работа с формами в представлениях
def post_share(request, post_id):
    # Извлечь пост по идентификатору id
    post = get_object_or_404(Post,
                            id=post_id,
                            status=Post.Status.PUBLISHED)

    sent = False # добавляем при Отправка электронных писем в представлениях

    if request.method == 'POST':
        # Форма была передана на обработку
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # Поля формы успешно прошли валидацию
            cd = form.cleaned_data
            # добавляем при Отправка электронных писем в представлениях    
            post_url = request.build_absolute_uri(
                    post.get_absolute_url())
            subject = f"{cd['name']} recommends you read " \
                    f"{post.title}"
            message = f"Read {post.title} at {post_url}\n\n" \
                    f"{cd['name']}\'s comments: {cd['comments']}"
            # Функция send_mail() принимает тему, сообщение, отправителя и список
            # получателей в качестве требуемых аргументов.
            send_mail(subject, message, 'your_account@gmail.com', [cd['to']])
            
            # Если вы используете SMTP-сервер, а не почтовый бэкенд1 console.Email-
            # Backend, то замените your_account@gmail.com своей реальной учетной записью
            # электронной почты.
            sent = True
            # ... отправить электронное письмо
    else:
        form = EmailPostForm()
    return render(request, 'blog/post/share.html', {'post': post,
                                                'form': form,
                                                'sent': sent}) # добавляем при Отправка электронных писем в представлениях

# Мы определили представление post_share, которое в качестве параметров
# принимает объект request и переменную post_id. Мы используем функцию
# сокращенного доступа get_object_or_404(), чтобы извлечь опубликованный
# пост по его id.
# Одно и то же представление используется как для отображения изначаль-
# ной формы на странице, так и для обработки представленных для валида-
# ции данных. HTTP-метод request позволяет различать случаи, когда форма
# передается на обработку. Запрос GET будет указывать на то, что пользователю
# должна быть отображена пустая форма, а запрос POST – на то, что форма пере-
# дается на обработку. Булево выражение request.method == 'POST' используется
# для того, чтобы проводить различие между этими двумя сценариями.

# 1. Когда страница загружается в первый раз, представление получает за-
# прос GET. В этом случае создается новый экземпляр класса EmailPostForm,
# который сохраняется в переменной form. Указанный экземпляр формы
# будет использоваться для отображения пустой формы в шаблоне:
# ```python
# form = EmailPostForm()
# ```
# 2. Когда пользователь заполняет форму и передает ее методом POST на
# обработку, создается экземпляр формы с использованием переданных
# данных, содержащихся в request.POST:
# ```python
# if request.method == 'POST':
#     # Форма была передана на обработку
#     form = EmailPostForm(request.POST)
# ```
# 3. После этого переданные данные валидируются методом is_valid() фор-
# мы. Указанный метод проверяет допустимость введенных в форму дан-
# ных и возвращает значение True, если все поля содержат валидные дан-
# ные. Если какое-либо поле содержит невалидные данные, то is_valid()
# возвращает значение False. Список ошибок валидации можно получить
# посредством form.errors.
# 4. Если форма невалидна, то форма снова прорисовывается в шаблоне,
# включая переданные данные. Ошибки валидации будут отображены
# в шаблоне.
# 5. Если форма валидна, то валидированные данные извлекаются посред-
# ством form.cleaned_data. Указанный атрибут представляет собой словарь
# полей формы и их значений.

# Мы реализовали представление отображения формы на странице и пере-
# дачи формы на обработку. Теперь мы научимся отправлять электронные
# письма с помощью
# Django и затем добавим эту функциональность в пред-
# ставление post_share.

# - - - - - - - - - - - - - - - - -
# Отправка электронных писем с помощью Django
# работаем в settings.py

# возвращаемся из setings.py
# Отредактируйте представление post_share в файле views.py приложения blog,
# как показано ниже:

# В приведенном выше исходном коде мы объявили переменную sent с изна-
# чальным значением False. Мы задаем этой переменной значение True после от-
# правки электронного письма. Позже мы будем использовать переменную sent
# в шаблоне отображения сообщения об успехе при успешной передаче формы.
# Поскольку ссылка на пост должна вставляться в электронное письмо, мы
# получаем абсолютный путь к посту, используя его метод get_absolute_url().
# Мы используем этот путь на входе в метод request.build_absolute_uri(), чтобы
# сформировать полный URL-адрес, включая HTTP-схему и хост-имя (hostname)1.
# Мы создаем тему и текст сообщения электронного письма, используя очи-
# щенные данные валидированной формы. Наконец, мы отправляем электрон-
# ное письмо на адрес электронной почты, указанный в поле to (Кому) формы.
# Теперь, когда представление post_share завершено, для него необходимо
# добавить новый шаблон URL-адреса.
# Откройте файл urls.py приложения blog и добавьте шаблон URL-адреса
# post_share, как показано ниже:

# - - - - - - - - - - - - - - - - -
# Создание системы комментариев
# Для того чтобы разработать такую систему, понадобится:
# •• модель комментария, чтобы хранить комментарии пользователей
# к постам;
# •• форма, которая позволяет пользователям передавать комментарии на
# обработку и управляет валидацией данных;
# •• представление, которое обрабатывает форму и сохраняет новый ком-
# ментарий в базе данных;
# •• список комментариев и форма, чтобы добавлять новый коммента-
# рий, который может быть вставлен в шаблон детальной информации
# о посте.

# Разработка модели комментария
# Откройте файл models.py приложения blog ...

# Далее
# Оперирование формами ModelForm в представлениях
@require_POST # декоратор require_POST, чтобы разрешить запросы методом POST только для этого представления.
def post_comment(request, post_id):
    post = get_object_or_404(Post,
                            id=post_id,
                            status=Post.Status.PUBLISHED)
    comment = None
    # Комментарий был отправлен
    form = CommentForm(data=request.POST) # создать экземпляр формы, 
                                        # используя переданные на обработку POST-данные
    if form.is_valid():
        # Создать объект класса Comment, не сохраняя его в базе данных, т.к. commit=False
        # Такой подход позволяет видоизменять объект 
        # перед его окончательным сохранением.
        comment = form.save(commit=False)
        # Назначить пост комментарию
        comment.post = post
        # Сохранить комментарий в базе данных
        comment.save()
    # Передаем объекты post, form и comment 
    # в контекст (еще не создан) шаблона blog/post/comment.html.
    return render(request, 'blog/post/comment.html',
                            {'post': post,
                            'form': form,
                            'comment': comment})
# Мы определили представление post_comment, которое принимает объект
# request и переменную post_id в качестве параметров. Мы будем использо-
# вать это представление, чтобы управлять передачей поста на обработку. Мы
# ожидаем, что форма будет передаваться с использованием HTTP-метода
# POST. Мы используем предоставляемый веб-фреймворком Django декоратор
# require_POST, чтобы разрешить запросы методом POST только для этого пред-
# ставления. Django позволяет ограничивать разрешенные для представлений
# HTTP-методы. Если пытаться обращаться к представлению посредством лю-
# бого другого HTTP-метода, то Django будет выдавать ошибку HTTP 405 (Метод
# не разрешен).

# В этом представлении реализованы следующие ниже действия.
# 1. По id поста извлекается опубликованный пост, используя функцию со-
# кращенного доступа get_object_or_404().
# 2. Определяется переменная comment с изначальным значением None. Ука-
# занная переменная будет использоваться для хранения комментарного
# объекта при его создании.
# 3. Создается экземпляр формы, используя переданные на обработку POST-
# данные, и проводится их валидация методом is_valid(). Если форма
# невалидна, то шаблон прорисовывается с ошибками валидации.
# 4. Если форма валидна, то создается новый объект Comment, вызывая ме-
# тод save() формы, и назначается переменной comment, как показано
# ниже:
# comment = form.save(commit=False)
# 5. Метод save() создает экземпляр модели, к которой форма привязана,
# и сохраняет его в базе данных. Если вызывать его, используя commit=False,
# то экземпляр модели создается, но не сохраняется в базе данных. Такой
# подход позволяет видоизменять объект перед его окончательным со-
# хранением.
# 6. Пост назначается созданному комментарию:
# comment.post = post
# 7. Новый комментарий создается в базе данных путем вызова его метода
# save():
# comment.save()
# 8. Прорисовывается шаблон blog/post/comment.html, передавая объекты
# post, form и comment в контекст шаблона. Этот шаблон еще не существует;
# мы создадим его позже.

# Давайте создадим шаблон URL-адреса этого представления.
# Отредактируйте файл urls.py приложения blog, добавив следующий ниже
# шаблон URL-адреса:

# - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Полнотекстовый поиск. Разработка представления поиска

def post_search(request):
    form = SearchForm()
    query = None
    results = []
    
    if 'query' in request.GET:
        form = SearchForm(request.GET)
        if form.is_valid():
            query = form.cleaned_data['query']
            # В приведенном коде создается объект SearchQuery, по нему
            # фильтруются результаты, и для упорядочивания результатов по релевант-
            # ности используется SearchRank.
            search_vector = SearchVector('title', weight='A', config='english') + \
                            SearchVector('body', weight='B', config='english')
                            # weight - Взвешивание запросов
            search_query = SearchQuery(query, config='english')
            # config - Выделение основ слов и удаление стоп-слов на определ. языках
            results = Post.published.annotate(search=search_vector,
                                            rank=SearchRank(search_vector, search_query)
                                            ).filter(rank__gte=0.3).order_by('-rank')
            # results = Post.published.annotate(
            #                                 similarity=TrigramSimilarity('title', query),
            #                                 ).filter(similarity__gt=0.1).order_by('-similarity')
    return render(request,
                'blog/post/search.html',
                {'form': form,
                'query': query,
                'results': results}
                )
    #     В приведенном выше представлении сначала создается экземпляр формы
    # SearchForm. Для проверки того, что форма была передана на обработку, в сло-
    # варе request.GET отыскивается параметр query. Форма отправляется методом
    # GET, а не методом POST, чтобы результирующий URL-адрес содержал пара-
    # метр query и им было легко делиться. После передачи формы на обработку
    # создается ее экземпляр, используя переданные данные GET, и проверяется
    # валидность данных формы. Если форма валидна, то с помощью
    # конкретно-
    # прикладного экземпляра SearchVector, сформированного с использованием
    # полей title и body, выполняется поиск опубликованных постов.
    # Теперь представление поиска готово и необходимо создать шаблон ото-
    # бражения формы и результатов при выполнении пользователем поиска.
    # Внутри каталога templates/blog/post/ создайте новый файл,